详解 | 为可折叠设备构建响应式 UI
为可折叠设备和大屏设备优化您的应用
Android 设备的屏幕尺寸日新月异,随着平板和可折叠设备的普及度越来越高,在开发响应式用户界面时,了解您应用的窗口尺寸和状态显得尤为重要。Jetpack WindowManager 现已进入 beta 测试阶段,这个库提供了与 Android 框架中 WindowManager 比较相似的功能,包括了对支持响应式 UI、检测屏幕改变的回调适配器和测试窗口 API 的支持。但 Jetpack WindowManager 还新增了对可折叠设备和 ChromeOS 这类窗口环境的支持。
Jetpack WindowManager
https://developer.android.google.cn/jetpack/androidx/releases/windowWindowManager
https://developer.android.google.cn/reference/android/view/WindowManager
新的 WindowManager API 包含了以下内容:
WindowLayoutInfo: 包含了窗口的显示特性,例如该窗口是否可折叠或包含铰链 https://developer.android.google.cn/reference/androidx/window/layout/WindowLayoutInfo
FoldingFeature: 让您能够监听可折叠设备的折叠状态得以判断设备的姿态 https://developer.android.google.cn/reference/androidx/window/layout/FoldingFeature WindowMetrics: 提供当前窗口或全部窗口的显示指标 https://developer.android.google.cn/reference/androidx/window/layout/WindowMetrics
Jetpack WindowManager 不与 Android 绑定,这让 API 能够迅速地迭代以支持快速发展的市场,还让开发者们能够通过更新库而不必等待 Android 版本更新来获得支持。
现在,Jetpack WindowManager 库已进入 beta 测试阶段,我们鼓励所有开发者来使用 Jetpack WindowManager,其与设备无关 API、测试 API 以及它引入的 WindowMetrics,使您的应用能够轻松响应窗口尺寸的变化。已经进入 beta 测试阶段,意味着您可以安心地专注于在这些设备上打造激动人心的体验,Jetpack WindowManager 最低支持到 API 14。
关于 Jetpack WindowManager
Jetpack WindowManager 是一个以 Kotlin 优先的现代化库,它支持不同形态的新设备,并提供 "类 AppCompat" 的功能以构建具有响应式 UI 的应用。
折叠状态
https://developer.android.google.cn/stories/apps/google-duo
△ 折叠状态: FLAT 和 HALF-OPENED
FLAT (展平)
https://developer.android.google.cn/reference/androidx/window/layout/FoldingFeature.State.Companion#FLAT()HALF_OPENED (半开)
https://developer.android.google.cn/reference/androidx/window/layout/FoldingFeature.State.Companion#HALF_OPENED()
lifecycleScope.launch(Dispatchers.Main) {
// 传递给 repeatOnLifecycle 的代码块将在生命周期进入 STARTED 时执行
// 并在生命周期为 STOPPED 时取消
// repeatOnLifecycle 将会在生命周期再次进入 STARTED 时自动重启代码块
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 当生命周期处于 STARTED 时安全地从 windowInfoRepository 中收集数据
// 当生命周期进入 STOPPED 时停止收集数据
windowInfoRepository.windowLayoutInfo
.collect { newLayoutInfo ->
updateStateLog(newLayoutInfo)
updateCurrentState(newLayoutInfo)
}
}
}
private fun isTableTopMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
WindowLayoutInfo
https://developer.android.google.cn/reference/androidx/window/layout/WindowLayoutInfo
FoldingFeature
https://developer.android.google.cn/reference/androidx/window/layout/FoldingFeature方向
https://developer.android.google.cn/reference/androidx/window/layout/FoldingFeature#orientation()isSeparating
https://developer.android.google.cn/reference/androidx/window/layout/FoldingFeature#isSeparating()
或者书本模式 (屏幕半开并且铰链处于垂直方向):
private fun isBookMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
注意: 在主线程/UI 线程中收集事件这点十分重要,这能避免在 UI 和事件处理之间的同步问题。
支持响应式 UI
API 30 当中的 WindowMetrics API
https://developer.android.google.cn/reference/android/view/WindowMetrics
WindowMetrics
https://developer.android.google.cn/reference/androidx/window/layout/WindowMetricsWindowMetricsCalculator
https://developer.android.google.cn/reference/androidx/window/layout/WindowMetricsCalculator
val windowMetrics =
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
onMeasure
https://developer.android.google.cn/reference/android/view/View#onMeasure(int,%20int)
另一个使用场景是用于测试中 (详见下面的测试一节)。
在处理应用 UI 的高阶用法中,通过该库提供的 WindowInfoRepository#currentWindowMetrics 能够在窗口尺寸变更时收到通知,这与是否触发配置变更无关。
这个例子是关于如何根据可用区域来切换您的布局:
// 因为 repeatOnLifecycle 是挂起函数,所以创建一个新的协程
lifecycleScope.launch(Dispatchers.Main) {
// 传递给 repeatOnLifecycle 的代码块将在生命周期进入 STARTED 时执行
// 并在生命周期为 STOPPED 时取消
// 它将会在生命周期再次进入 STARTED 时自动重启
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 当生命周期处于 STARTED 时安全地从 windowInfoRepository 中收集数据
// 当生命周期进入 STOPPED 时停止收集数据
windowInfoRepository.currentWindowMetrics
.collect { windowMetrics ->
val currentBounds = windowMetrics.bounds
Log.i(TAG, "New bounds: {$currentBounds}")
// 我们可以根据需要在这里更新布局
}
}
}
回调适配器
要在 Java 编程语言中使用这个库或者使用回调接口,请在您的应用中添加 androidx.window:window-java 依赖。该组件提供了 WindowInfoRepositoryCallbackAdapter,您可以通过它注册 (取消注册) 一个用以接收设备姿态及窗口指标信息更新的回调。
androidx.window:window-java
https://developer.android.google.cn/jetpack/androidx/releases/window#declaring_dependenciesWindowInfoRepositoryCallbackAdapter
https://developer.android.google.cn/reference/androidx/window/java/layout/WindowInfoRepositoryCallbackAdapter
public class SplitLayoutActivity extends AppCompatActivity {
private WindowInfoRepositoryCallbackAdapter windowInfoRepository;
private ActivitySplitLayoutBinding binding;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
windowInfoRepository =
new WindowInfoRepositoryCallbackAdapter(WindowInfoRepository.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart();
windowInfoRepository.addWindowLayoutInfoListener(Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoRepository.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo windowLayoutInfo) {
binding.splitLayout.updateWindowLayout(windowLayoutInfo);
}
}
}
测试
该库在 androidx.window:window-testing 中提供了 WindowLayoutInfoPublisherRule 让您能够发布一个 WindowInfoLayout 以支持测试 FoldingFeature:
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
androidx.window:window-testing
https://developer.android.google.cn/jetpack/androidx/releases/window#declaring_dependenciesWindowLayoutInfoPublisherRule
https://developer.android.google.cn/reference/androidx/window/testing/layout/WindowLayoutInfoPublisherRule
我们可以在测试中虚拟一个 FoldingFeature:
val feature = FoldingFeature(
activity = activity,
center = center,
size = 0,
orientation = VERTICAL,
state = HALF_OPENED
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
publisherRule.overrideWindowLayoutInfo(expected)
然后使用 WindowLayoutInfoPublisherRule 来发布它:
val publisherRule = WindowLayoutInfoPublisherRule()
publisherRule.overrideWindowLayoutInfo(expected)
最后,使用可用的 Espresso 匹配器来检查我们正在测试的 Activity 的布局是否符合预期。
https://developer.android.google.cn/training/testing/espresso/cheat-sheet
下面这个测试中发布了一个处于 HALF_OPENED 状态并且铰链垂直于屏幕中心的 FoldingFeature:
@Test
fun testDeviceOpen_Vertical(): Unit = testScope.runBlockingTest {
activityRule.scenario.onActivity { activity ->
val feature = FoldingFeature(
activity = activity,
orientation = VERTICAL,
state = HALF_OPENED
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()
val value = testScope.async {
activity.windowInfoRepository().windowLayoutInfo.first()
}
publisherRule.overrideWindowLayoutInfo(expected)
runBlockingTest {
Assert.assertEquals(
expected,
value.await()
)
}
}
// 检查在有垂直折叠特性时 start_layout 在 end_layout 的左侧
// 这需要在足够大的屏幕上运行测试以适应屏幕上的两个视图
onView(withId(R.id.start_layout))
.check(isCompletelyLeftOf(withId(R.id.end_layout)))
}
查看示例代码
Github 上的最新示例展示了如何使用 Jetpack WindowManager 库从 WindowLayoutInfo 流收集信息,或者通过向 WindowInfoRepositoryCallbackAdapter 注册回调来获取显示姿态信息。
该实例还包含一些测试,它们可以在任何设备或模拟器中运行。
在您的应用中使用 WindowManager
欢迎反馈,让我们听到您的声音!
https://issuetracker.google.com/issues/new?component=840395&template=1412556 更多关于为可折叠设备和其它大屏幕设备进行优化的资源,请参阅:
https://developer.android.google.cn/large-screens
也欢迎您通过下方二维码向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!
推荐阅读